package main;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;

import javax.management.InvalidAttributeValueException;

import util.Tuple;
import eduni.simjava.Sim_port;
import eduni.simjava.Sim_system;
import eduni.simjava.distributions.Sim_negexp_obj;
import events.Client;
import events.CommonServer;
import events.EventType;
import events.LAN;
import events.RequestHeader;
import events.ServerPool;
import events.Source;

public class Main {
       
        /**
         * This method will create five files to be requested by clients during simulations
         * with a size mean equals to the one provided by the user
         * @param fileSizeMean
         * @return
         */
        public static HashMap<String, Double> generateFiles(double fileSizeMean){
                String filesName = "F";
               
                //Creating the file with size exactly equals to the mean
                HashMap<String, Double> files = new HashMap<String, Double>();
                files.put(filesName+1, fileSizeMean*0.75);
                files.put(filesName+2, fileSizeMean*1.25);
                files.put(filesName+3, fileSizeMean);
                files.put(filesName+4, fileSizeMean*0.5);
                files.put(filesName+5, fileSizeMean*1.5);
               
                return files;
        }
       
        /**
         * This method will obtain the simulation parameters from the user and prepare simulation
         *
         * @param args
         * @throws IOException
         * @throws InvalidAttributeValueException
         */
        public static void main(String[] args) throws IOException, InvalidAttributeValueException {
                if (args.length != 7) {
                        System.err.println("Usage: <number of Clients> <file mean size in KB> <interarrival mean> <bandwidth at server in KB / time unit> <bandwith at LAN in KB / time unit> <number of requests to be fulfilled> <simulation mode: P2P or CS>");
                        System.err.println("Example: 10 1024 2 10240 10240 100 CS");
                        System.exit(1);
                }
               
                Sim_system.initialise();
               
                int count = 0;
                //Number of clients in the system that will generate requests
                int numberOfClients = Integer.valueOf(args[count++]);
                // The size of the file being requested by the clients
                double fileSizeMean = Double.valueOf(args[count++]);
                // The arrival rate of the clients at the server (this value is used as mean for the Poisson distribution)
                double arrivalRate = Double.valueOf(args[count++]);
                // The bandwidth of the server
                double serverBandwidth = Double.valueOf(args[count++]);
                //The bandwith at LAN
                double LANBandwidth = Double.valueOf(args[count++]);
                // The number of requests to be fulfilled
                long requestsFulfilled = Long.valueOf(args[count++]);
               
                String simulationMode = args[count++].toLowerCase();
               
                //Creating files to be downloaded
                HashMap<String, Double> filesSizes = generateFiles(fileSizeMean);
               
                //Creating list of clients and correspondents servers
                List<Client> clients = new ArrayList<Client>(numberOfClients);
                List<CommonServer> clientsAsServers = new ArrayList<CommonServer>(numberOfClients);
               
                for (int i = 0; i < numberOfClients; i++){
                       
                        Client client = new Client("Client"+i, filesSizes, arrivalRate);
                        clients.add(client);
                       
                        if (simulationMode.equals("p2p")){
                                CommonServer clientAsServer = new CommonServer("ClientServer"+i, serverBandwidth, new HashMap<String, Set<Integer>>());
                                clientsAsServers.add(clientAsServer);
                        }
                }
               
                //Determining blocks of files that will be available on server
                HashMap<String, Set<Integer>> filesAvailable = getBlocksOfFilesAvailable(filesSizes);
               
                //Creating source to determine clients arrivals, first server, LAN and ServerPool
                Source source = new Source("Source", fileSizeMean, arrivalRate, clients);
                CommonServer defaultServer = new CommonServer("Server", serverBandwidth, filesAvailable);
               
                LAN lan = new LAN("LAN", LANBandwidth);
                lan.createConnectionForClients(clients);
               
                ServerPool serverPool = ServerPool.getInstance();
                HashMap<CommonServer, Sim_port> serverPoolOutPorts = createClientsAsServersPorts(clientsAsServers, defaultServer);
                serverPool.setServerPorts(serverPoolOutPorts);
               
                //Adding servers to pool
                serverPool.addServer(defaultServer);
                for (CommonServer clientServer : clientsAsServers){
                        serverPool.addServer(clientServer);
                }
               
                // Adding entities to the simulation
                Sim_system.add(source);
                Sim_system.add(defaultServer);
                Sim_system.add(serverPool);
               
                for (Client client : clients){
                        Sim_system.add(client);
                }
               
                for (CommonServer clientAsServer : clientsAsServers){
                        Sim_system.add(clientAsServer);
                }
               
                //Linking ports
                linkPorts(clients, clientsAsServers, lan, serverPoolOutPorts);
               
                //Setting final details of trace and stop conditions
//              Sim_system.set_trace_detail(true, true, true);
//              Sim_system.set_termination_condition(Sim_system.INTERVAL_ACCURACY,
//                Sim_system.IND_REPLICATIONS,
//                0.95,
//                0.08,
//                "Server",
//                Sim_stat.UTILISATION);
                Sim_system.set_report_detail(true, true);
                Sim_system.set_trace_detail(true, true, true);
                Sim_system.set_termination_condition(Sim_system.EVENTS_COMPLETED, "LAN", EventType.REQUEST_FILE, requestsFulfilled,
                                false);
                Sim_system.generate_graphs(true);
               
                //Starting simulation
                Sim_system.run();
               
                //Printing a report
                createReport(clients);
        }

        private static HashMap<String, Set<Integer>> getBlocksOfFilesAvailable(
                        HashMap<String, Double> filesSizes) {
                HashMap<String, Set<Integer>> filesAvailable = new HashMap<String, Set<Integer>>();
                for(String file : filesSizes.keySet()){
                        Double size = filesSizes.get(file);
                        int numberOfBlocks = (int) (size / RequestHeader.FILE_BLOCK_SIZE);
                       
                        Set<Integer> availableBlocks = new HashSet<Integer>();
                        for(int i = 0; i < numberOfBlocks; i++){
                                availableBlocks.add(i);
                        }
                       
                        filesAvailable.put(file, availableBlocks);
                }
                return filesAvailable;
        }

        /**
         * This method is responsible for linking all simulation ports
         * @param clients
         * @param clientsAsServers
         * @param lan
         * @param serverPoolOutPorts
         */
        private static void linkPorts(List<Client> clients, List<CommonServer> clientsAsServers, LAN lan, HashMap<CommonServer, Sim_port> serverPoolOutPorts) {
                Sim_system.link_ports("Source", "out", "ServerPool", "in");
                Sim_system.link_ports("ServerPool", "outToPool", "ServerPool", "in");
               
                for(CommonServer actualServer : serverPoolOutPorts.keySet()){
                        Sim_port actualServerPort = serverPoolOutPorts.get(actualServer);
                        Sim_system.link_ports("ServerPool", actualServerPort.get_pname() , actualServer.get_name(), "in");
                }
               
                for(Client client : clients){
                        Sim_system.link_ports(client.get_name(), "outToServer", "ServerPool", "in");
                }

                Sim_system.link_ports("Server", "outToLan", "LAN", "in");
                for(CommonServer clientAsServer : clientsAsServers){
                        Sim_system.link_ports(clientAsServer.get_name(), "outToLan", "LAN", "in");
                }
               
                HashMap<Client, Sim_port> lanClientsPorts = lan.getPorts();
                for (Client client : lanClientsPorts.keySet()){
                        Sim_system.link_ports("LAN", "outTo"+client.get_name(), client.get_name(), "inFromLan");
                }
        }
       
        /**
         * This method will create the output ports that will connect the server pool with each server
         * @param clientsAsServers The clients that will become servers
         * @param defaultServer The first server of the system
         * @return
         */
        private static HashMap<CommonServer, Sim_port> createClientsAsServersPorts(List<CommonServer> clientsAsServers, CommonServer defaultServer) {
                HashMap<CommonServer, Sim_port> ports = new HashMap<CommonServer, Sim_port>();
               
                //Creating ports for clients as servers
                for (CommonServer server : clientsAsServers){
                        Sim_port port = new Sim_port("outToServer"+server.get_name());
                        ports.put(server, port);
                }
               
                //Creating port for the default server
                Sim_port port = new Sim_port("outToServer"+defaultServer.get_name());
                ports.put(defaultServer, port);
               
                return ports;
        }


        /**
         * This method will create the output files containing the amount of blocks/files downloaded per time and
         * the response time for each download
         * @param clients The clients that made downloads
         * @throws IOException
         */
        private static void createReport(List<Client> clients) throws IOException {
                BufferedWriter blocksWriter = new BufferedWriter(new FileWriter("blocks.dat"));
                StringBuffer blocksBuffer = new StringBuffer();
                BufferedWriter filesWriter = new BufferedWriter(new FileWriter("files.dat"));
                StringBuffer filesBuffer = new StringBuffer();
                for(Client client : clients){
                        List<Tuple<Double, Integer>> blocksPerTime = client.getBlocksPerTime();
                        for(Tuple<Double, Integer> tuple : blocksPerTime){
                                blocksBuffer.append(client.get_name()+"\t"+tuple.getFirstElement()+"\t"+tuple.getSecondElement()+"\n");
                        }
                       
                        List<Tuple<Double, Integer>> filesPerTime = client.getFilesPerTime();
                        for(Tuple<Double, Integer> tuple : filesPerTime){
                                filesBuffer.append(client.get_name()+"\t"+tuple.getFirstElement()+"\t"+tuple.getSecondElement()+"\n");
                        }
                }
               
                blocksWriter.write(blocksBuffer.toString().trim());
                filesWriter.write(filesBuffer.toString().trim());
               
                blocksWriter.close();
                filesWriter.close();
               
                //Printing a report for download durations
                BufferedWriter durationWriter = new BufferedWriter(new FileWriter("durations.dat"));
                StringBuffer durationBuffer = new StringBuffer();
               
                for (Client client : clients){
                        HashMap<String, List<Double>> durations = client.getDurations();
                       
                        if(durations.size() > 0){
                                for(Entry<String, List<Double>> entry : durations.entrySet()){
                                        String fileName = entry.getKey();
                                        for(double duration : entry.getValue()){
                                                durationBuffer.append(client.get_name()+"\t"+fileName+"\t"+duration+"\n");
                                        }
                                }
                        }
                }
                durationWriter.write(durationBuffer.toString().trim());
                durationWriter.close();
        }
}
